feat: read-only shared map viewer with password gate - #469
Merged
Conversation
Stages 4-5 of the read-only private maps feature (plan in READ_ONLY_PRIVATE_MAPS.md): - /share/[token] page: resolves the share, mints the grant cookie via a claim route handler for passwordless shares (cookies cannot be set during page render), renders the password form for protected ones, and seeds the map query cache server-side - Password gate: SharePasswordForm + POST /api/share/[token]/verify with scrypt verification and per-IP+token Redis rate limiting - Read-only shell: ReadOnlyNavbar (name, view switcher, area search — no thumbnail upload or initial-view writes), ReadOnlyMapControls (boundary hover info, inspector, style/zoom/timeline), LegendDisplay (display-only choropleth legend) + existing MarkerLegend - isReadOnlyRouteAtom + useMapEditable(); inspector hides its config gears, "Add to areas" and "View in table" in read-only mode - useMapViews skips server writes on the read-only route so map style and timeline changes stay client-side only - useDataSources uses listForMapView for anonymous viewer routes - Markers API honours the properties param for grant holders whose share covers the data source, so marker styling matches the editor Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The passwordless claim flow was page -> 307 claim -> 307 page?c=1, with the c=1 marker guarding against redirect loops when cookies are blocked. The marker leaked into the address bar, so a viewer copying the URL to share it passed on a link that showed the cookies-required message to new visitors without ever attempting a claim. The claim endpoint is now a POST returning 204 (mirroring verify), and the page renders a ShareClaim component that fetches it and refreshes - the same mechanism as SharePasswordForm, minus the form. The URL never changes. Blocked cookies are detected without URL state: a sessionStorage timestamp catches remount loops, and a finished refresh that still renders ShareClaim means the cookie did not stick. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ReadOnlyNavbar sits inside the share page's pointer-events-none overlay and never restored pointer events, so the view switcher and search box were click-transparent. Wrap it in pointer-events-auto, as MapNavbar does inside the same overlay pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
joaquimds
force-pushed
the
feat/read-only-map-viewer
branch
from
August 5, 2026 17:58
4f678a4 to
d98af83
Compare
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…im guard - Scope ShareClaim's sessionStorage claim-attempt key per token so a claim on one share cannot flag a different share's link as blocked - Detect blocked cookies in SharePasswordForm the same way ShareClaim does (refresh inside a transition; surviving a finished refresh means the grant cookie did not stick) instead of silently re-showing the form - Treat a 404 from verify (share disabled / token rotated) as a refresh to the real 404 page rather than reporting an incorrect password Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The frontend for read-only private map sharing — stages 4–5 of the plan in
READ_ONLY_PRIVATE_MAPS.md, building on the backend merged in #468. Adds the/share/[token]viewer route and the password gate. (Stage 6 — the share dialog, feature flag, and toggle rename — follows in a final PR; until it lands, shares can only be created via themapSharetRPC router.)The viewer (
/share/[token])A chrome-free version of the private map view for anonymous recipients:
notFound()if missing/disabled → checks the grant cookie → renders the map shell, the password form, or redirects through the claim endpoint. The map is fetched server-side (the grant cookie authorises the tRPC caller) and seeded into the React Query cache, same pattern as the public map page.robots: noindexon the route.?viewId=), matching the whole-map share scope.Grant minting (who sets the cookie, and when)
ShareClaim, which POSTs to/api/share/[token]/claim(mints the grant cookie, returns 204 — exists because Next.js can't set cookies during page render) and then refreshes, after which the server component sees the grant — the same mechanism as the password flow, minus the form. The URL never changes. Blocked cookies are detected without URL state (a sessionStorage timestamp catches remount loops; a finished refresh still renderingShareClaimmeans the cookie didn't stick) and show a friendly message instead of looping.SharePasswordForm;POST /api/share/[token]/verifychecks scrypt, rate-limited 5 attempts / 15 min per IP + token via the existing Redis limiter, and mints on success. The form then refreshes, and the server component sees the grant.Keeping read-only actually read-only
The private map tree assumes an editing user in several places; each is handled:
ReadOnlyNavbarexists because the private navbar writes on mount (auto thumbnail upload, initial-view creation) — neither is mounted.useMapViewsnow skipsmap.updateViews/mapView.deleteon the read-only route: map-style and timeline changes still update the query cache (instant feedback) but are never persisted and reset on reload.useMapEditable()hook (backed byisReadOnlyRouteAtom, following theisPublicMapRouteAtomprecedent). The Notes tab already self-gates on org membership.useDataSourcesusesdataSource.listForMapView(grant-authorised) instead of theprotectedProcedurelistReadableon anonymous viewer routes.LegendDisplayis a new display-only choropleth legend (bars + labels, reusingLegendBars/BivariateLegend); the 790-line editorLegendis untouched.MarkerLegendwas already write-free and is reused as-is.propertiesparam for grant holders whose share covers the data source — so shared maps look exactly like the editor, while public-map/public-data-source anonymous requests stay on minimal properties.Test plan
npm run lintclean (prettier, eslint, tsc, madge)mapShare.enable, open the link logged out — map renders read-only with legend/hover/inspector/zoom/style/timeline/search; view switching works; no write requests in the network tabNotes for reviewers
cookies()+ already-tested primitives); the manual checkpoints above cover them.🤖 Generated with Claude Code